﻿using System;
using System.Collections.Generic;
using System.Linq;

using Duality;
using OpenTK;

namespace ClientApp
{
	public sealed class TuioListener : TUIO.TuioListener
	{
		private enum TuioEventType
		{
			Unknown,

			AddCursor,
			AddObject,
			RemoveCursor,
			RemoveObject,
			UpdateCursor,
			UpdateObject
		}
		private class TuioEvent
		{
			private TuioInstance instance = null;
			private TuioEventType type = TuioEventType.Unknown;

			public TuioInstance Instance
			{
				get { return this.instance; }
			}
			public TuioEventType Type
			{
				get { return this.type; }
			}

			public TuioEvent(TuioEventType type, TuioInstance instance)
			{
				this.type = type;
				this.instance = instance;
			}
		}

		private static TuioListener instance;
		public static TuioListener Instance
		{
			get
			{
				if (instance == null) instance = new TuioListener();
				return instance;
			}
		}

		private TUIO.TuioClient tuioClient = null;
		private List<TuioEvent> eventBuffer = new List<TuioEvent>();
		private List<TuioInstance> tuioInstList = new List<TuioInstance>();

		public event EventHandler<TuioEventArgs> AddTuioCursor = null;
		public event EventHandler<TuioEventArgs> RemoveTuioCursor = null;
		public event EventHandler<TuioEventArgs> UpdateTuioCursor = null;
		public event EventHandler<TuioEventArgs> AddTuioObject = null;
		public event EventHandler<TuioEventArgs> RemoveTuioObject = null;
		public event EventHandler<TuioEventArgs> UpdateTuioObject = null;

		public bool IsConnected
		{
			get { return this.tuioClient != null && this.tuioClient.isConnected(); }
		}
		public IEnumerable<TuioInstance> TuioInstances
		{
			get { return this.tuioInstList; }
		}

		private TuioListener()
		{
			if (DualityApp.ExecContext == DualityApp.ExecutionContext.Game)
				this.Connect();
		}

		public void Connect()
		{
			tuioClient = new TUIO.TuioClient();
			tuioClient.addTuioListener(this);
			tuioClient.connect();
		}
		public void Disconnect()
		{
			if (tuioClient == null) return;
			if (tuioClient.isConnected()) tuioClient.disconnect();
			tuioClient = null;
		}

		public void Update()
		{
			lock (this.eventBuffer)
			{
				// Process TUIO event queue
				foreach (TuioEvent ev in this.eventBuffer)
				{
					switch (ev.Type)
					{
						case TuioEventType.AddCursor: this.OnAddTuioCursor(ev.Instance); break;
						case TuioEventType.RemoveCursor: this.OnRemoveTuioCursor(ev.Instance); break;
						case TuioEventType.UpdateCursor: this.OnUpdateTuioCursor(ev.Instance); break;
						case TuioEventType.AddObject: this.OnAddTuioObject(ev.Instance); break;
						case TuioEventType.RemoveObject: this.OnRemoveTuioObject(ev.Instance); break;
						case TuioEventType.UpdateObject: this.OnUpdateTuioObject(ev.Instance); break;
					}
				}
				this.eventBuffer.Clear();

				// For stability reasons, do a consistency check between TUIO-internal data and TuioListener data.
				// This is normally 100% redundant, but will help recover from out-of-sync bugs.
				TuioInstance[] tiArray = this.tuioInstList.ToArray();
				TUIO.TuioCursor[] tcurArray = this.tuioClient.getTuioCursors().ToArray();
				TUIO.TuioObject[] tobjArray = this.tuioClient.getTuioObjects().ToArray();
				foreach (TUIO.TuioCursor tcur in tcurArray)
				{
				    if (!this.tuioInstList.Any(ti => ti.Equals(tcur)))
				        this.OnAddTuioCursor(new TuioInstance(tcur));
				}
				foreach (TUIO.TuioObject tobj in tobjArray)
				{
				    if (!this.tuioInstList.Any(ti => ti.Equals(tobj)))
				        this.OnAddTuioObject(new TuioInstance(tobj));
				}
				foreach (TuioInstance ti in tiArray)
				{
				    if (!tcurArray.Any(tcur => ti.Equals(tcur)) && !tobjArray.Any(tobj => ti.Equals(tobj)))
				    {
				        if (ti.Type == TuioType.Cursor)
				            this.OnRemoveTuioCursor(ti);
				        else
				            this.OnRemoveTuioObject(ti);
				    }
				}
			}
		}

		private void OnAddTuioCursor(TuioInstance instance)
		{
			this.tuioInstList.Add(instance);

			if (this.AddTuioCursor != null)
				this.AddTuioCursor(this, new TuioEventArgs(instance));
		}
		private void OnRemoveTuioCursor(TuioInstance instance)
		{
			this.tuioInstList.Remove(instance);

			if (this.RemoveTuioCursor != null)
				this.RemoveTuioCursor(this, new TuioEventArgs(instance));
		}
		private void OnUpdateTuioCursor(TuioInstance instance)
		{
			if (this.UpdateTuioCursor != null)
				this.UpdateTuioCursor(this, new TuioEventArgs(instance));
		}
		private void OnAddTuioObject(TuioInstance instance)
		{
			this.tuioInstList.Add(instance);

			if (this.AddTuioObject != null)
				this.AddTuioObject(this, new TuioEventArgs(instance));
		}
		private void OnRemoveTuioObject(TuioInstance instance)
		{
			this.tuioInstList.Remove(instance);

			if (this.RemoveTuioObject != null)
				this.RemoveTuioObject(this, new TuioEventArgs(instance));
		}
		private void OnUpdateTuioObject(TuioInstance instance)
		{
			if (this.UpdateTuioObject != null)
				this.UpdateTuioObject(this, new TuioEventArgs(instance));
		}

		// Note that all TUIO.TuioListener methods are executed in a different thread. Don't directly interfere with Duality here.
		void TUIO.TuioListener.addTuioCursor(TUIO.TuioCursor tcur)
		{
			lock (this.eventBuffer)
			{
				TuioInstance instance = new TuioInstance(tcur);
				this.eventBuffer.Add(new TuioEvent(TuioEventType.AddCursor, instance));
			}
		}
		void TUIO.TuioListener.addTuioObject(TUIO.TuioObject tobj)
		{
			lock (this.eventBuffer)
			{
				TuioInstance instance = new TuioInstance(tobj);
				this.eventBuffer.Add(new TuioEvent(TuioEventType.AddObject, instance));
			}
		}
		void TUIO.TuioListener.refresh(TUIO.TuioTime ftime) { }
		void TUIO.TuioListener.removeTuioCursor(TUIO.TuioCursor tcur)
		{
			lock (this.eventBuffer)
			{
				TuioInstance instance = this.tuioInstList.FirstOrDefault(i => i.Equals(tcur));
				if (instance == null) return;

				this.eventBuffer.Add(new TuioEvent(TuioEventType.RemoveCursor, instance));
			}
		}
		void TUIO.TuioListener.removeTuioObject(TUIO.TuioObject tobj)
		{
			lock (this.eventBuffer)
			{
				TuioInstance instance = this.tuioInstList.FirstOrDefault(i => i.Equals(tobj));
				if (instance == null) return;

				this.eventBuffer.Add(new TuioEvent(TuioEventType.RemoveObject, instance));
			}
		}
		void TUIO.TuioListener.updateTuioCursor(TUIO.TuioCursor tcur)
		{
			lock (this.eventBuffer)
			{
				TuioInstance instance = this.tuioInstList.FirstOrDefault(i => i.Equals(tcur));
				if (instance == null) return;

				instance.UpdateFrom(tcur);
				this.eventBuffer.Add(new TuioEvent(TuioEventType.UpdateCursor, instance));
			}
		}
		void TUIO.TuioListener.updateTuioObject(TUIO.TuioObject tobj)
		{
			lock (this.eventBuffer)
			{
				TuioInstance instance = this.tuioInstList.FirstOrDefault(i => i.Equals(tobj));
				if (instance == null) return;

				instance.UpdateFrom(tobj);
				this.eventBuffer.Add(new TuioEvent(TuioEventType.UpdateObject, instance));
			}
		}
	}

	public enum TuioType
	{
		Unknown,

		Object,
		Cursor
	}

	public class TuioInstance
	{
		public struct PathNode
		{
			public Vector2 pos;
			public long time;

			public PathNode(Vector2 pos, long time)
			{
				this.pos = pos;
				this.time = time;
			}
			public PathNode(TUIO.TuioPoint point)
			{
				this.pos = new Vector2(point.getX(), point.getY());
				this.time = point.getStartTime().getTotalMilliseconds();
			}
		}

		private TuioType type;
		private int id;
		private long uniqueSessionId;
		private Vector2 pos;
		private Vector2 vel;
		private float motionSpeed;
		private float motionAcc;
		private float angle;
		private float rotSpeed;
		private float rotAcc;
		private List<PathNode> path;

		public TuioType Type
		{
			get { return this.type; }
		}
		public int Id
		{
			get { return this.id; }
		}
		public long UniqueSessionId
		{
			get { return this.uniqueSessionId; }
		}
		public Vector2 Pos
		{
			get { return this.pos; }
		}
		public Vector2 Vel
		{
			get { return this.vel; }
		}
		public float MotionSpeed
		{
			get { return this.motionSpeed; }
		}
		public float MotionAcc
		{
			get { return this.motionAcc; }
		}
		public float Angle
		{
			get { return this.angle; }
		}
		public float RotationSpeed
		{
			get { return this.rotSpeed; }
		}
		public float RotationAcc
		{
			get { return this.rotAcc; }
		}
		public IEnumerable<PathNode> Path
		{
			get { return this.path; }
		}

		internal TuioInstance(TUIO.TuioCursor cursor) { this.UpdateFrom(cursor); }
		internal TuioInstance(TUIO.TuioObject obj) { this.UpdateFrom(obj); }

		internal void UpdateFrom(TUIO.TuioCursor cursor)
		{
			this.type = TuioType.Cursor;
			this.id = cursor.getCursorID();
			this.uniqueSessionId = cursor.getSessionID();
			this.pos = new Vector2(cursor.getX(), cursor.getY());
			this.vel = new Vector2(cursor.getXSpeed(), cursor.getYSpeed());
			this.motionSpeed = cursor.getMotionSpeed();
			this.motionAcc = cursor.getMotionAccel();
			this.angle = 0.0f;
			this.rotSpeed = 0.0f;
			this.rotAcc = 0.0f;
			this.path = cursor.getPath().Select(t => new PathNode(t)).ToList();
		}
		internal void UpdateFrom(TUIO.TuioObject obj)
		{
			this.type = TuioType.Object;
			this.id = obj.getSymbolID();
			this.uniqueSessionId = obj.getSessionID();
			this.pos = new Vector2(obj.getX(), obj.getY());
			this.vel = new Vector2(obj.getXSpeed(), obj.getYSpeed());
			this.motionSpeed = obj.getMotionSpeed();
			this.motionAcc = obj.getMotionAccel();
			this.angle = obj.getAngle();
			this.rotSpeed = obj.getRotationSpeed();
			this.rotAcc = obj.getRotationAccel();
			this.path = obj.getPath().Select(t => new PathNode(t)).ToList();
		}

		internal bool Equals(TUIO.TuioContainer container)
		{
			return this.uniqueSessionId == container.getSessionID();
		}
	}

	public class TuioEventArgs : EventArgs
	{
		private TuioInstance instance;
		public TuioInstance Instance
		{
			get { return this.instance; }
		}
		public TuioEventArgs(TuioInstance instance)
		{
			this.instance = instance;
		}
	}
}
